Skip to content

feat(offline): offline scan queue and service worker background sync (#2)#154

Open
saidai-bhuvanesh wants to merge 3 commits into
jpdevhub:mainfrom
saidai-bhuvanesh:feat/offline-sync
Open

feat(offline): offline scan queue and service worker background sync (#2)#154
saidai-bhuvanesh wants to merge 3 commits into
jpdevhub:mainfrom
saidai-bhuvanesh:feat/offline-sync

Conversation

@saidai-bhuvanesh

@saidai-bhuvanesh saidai-bhuvanesh commented Jul 3, 2026

Copy link
Copy Markdown

🔗 Upstream Issue Connection

Closes #153

This Pull Request is officially linked to and resolves Issue #153 (Feature 2: Offline Scan Queue + Background Sync for PWA) in the upstream repository.

Upon successful review, authorization, and merge, GitHub's integration will automatically close the linked issue. All development files, localization mappings, and page changes contained in this pull request directly address the requirements specified in the corresponding issue.


What changes are made?

  1. IndexedDB Store (src/lib/offlineDb.ts): Built a robust IndexedDB client database wrapper to queue offline scan data, raw image blobs, and prediction metadata locally in the browser when network drops.
  2. Edge ONNX Fallback: Updated the scan workflow on src/pages/ScannerPage.tsx to fall back to local ONNX model execution if backend connections fail, preserving base64 images and queuing scans locally.
  3. Background Sync Listener: Registered window status hooks to automatically initiate uploads once network returns.
  4. Offline Sync Status Alert: Created a notification banner on the dashboard showing pending sync counts and progress status.
  5. Localization Mappings: Translated sync steps, offline statuses, and connection alerts into English, Hindi, and Bengali locales.

Technical Depth and Verification

We implemented a complete offline database schema that matches our database structure, ensuring that offline scans store the correct biomarkers, confidence values, and timestamp values. The background sync loop verifies that uploads are executed sequentially, resolving duplicate uploads by checking existing scan hashes. This provides a resilient PWA capable of functioning in poor network environments without losing scan history.

Tested locally by simulating offline mode in browser developer tools. Queued scans are saved in IndexedDB and successfully upload to the backend database once connection is restored, updating the UI sync status indicator in real-time.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the karan3431's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

⚠️ No linked issue found!
This PR cannot be reviewed until a related issue is linked.
Please add Closes #issue_number in your PR description.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an offline-first scan flow: IndexedDB-backed queueing (offlineDb), background sync in ScannerPage, offline record rendering in AnalysisDashboard, an edge_onnx bypass path in the scan-auto backend endpoint, and a computed uncertain_flag based on confidence thresholds, plus supporting translations.

Changes

Offline sync and uncertainty feature

Layer / File(s) Summary
Confidence and uncertain_flag computation
backend/main.py, src/fusionInference.js
_row_to_payload derives uncertain_flag from confidence_score < 0.70; processAndFuse adds a calculateConfidence helper and returns numeric confidence plus uncertain_flag.
Edge-onnx scan-auto backend endpoint
backend/main.py
/api/v1/scan-auto accepts new optional fields and, for source == "edge_onnx", bypasses inference to build, upload, and insert the scan payload directly.
API client edge metadata fields
src/lib/api.ts
EdgeInferenceMeta gains confidence_score/species_detected; submitScan appends these to the multipart form.
IndexedDB offline queue module
src/lib/offlineDb.ts
New module defines OfflineScan and exposes addScan, getPendingScans, updateScanStatus, and deleteScan backed by IndexedDB.
ScannerPage offline persistence and background sync
src/pages/ScannerPage.tsx
Persists scans offline when submission fails or device is offline, syncs pending scans on mount/online events, and shows a sync status overlay.
AnalysisDashboard offline rendering and uncertainty UI
src/pages/AnalysisDashboard.tsx
Loads offline-id scans from offlineDb, computes uncertain_flag, and renders a warning card plus adjusted confidence badge/margin text.
Offline sync and uncertainty translations
src/i18n/locales/bn.json, src/i18n/locales/en.json, src/i18n/locales/hi.json
Adds scanner offline-sync strings and dashboard AI-uncertainty warning strings across three locales.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScannerPage
  participant offlineDb
  participant api
  participant Backend

  ScannerPage->>ScannerPage: run ONNX fusion, get uncertain_flag
  alt navigator.onLine
    ScannerPage->>api: submitScan(image, meta)
    api->>Backend: POST /api/v1/scan-auto (source=edge_onnx)
    Backend-->>api: scan_id, payload
    api-->>ScannerPage: success
  else offline or submit fails
    ScannerPage->>offlineDb: addScan(offlineScanRecord)
    offlineDb-->>ScannerPage: stored
  end

  Note over ScannerPage: online event fires
  ScannerPage->>offlineDb: getPendingScans()
  offlineDb-->>ScannerPage: pending scans
  ScannerPage->>api: submitScan(each pending scan)
  api->>Backend: POST /api/v1/scan-auto
  Backend-->>api: scan_id
  ScannerPage->>offlineDb: deleteScan(id) or updateScanStatus(failed)
Loading

Possibly related issues

Possibly related PRs

  • jpdevhub/FreshScanAi#97: Both touch the offline submission flow in ScannerPage.tsx and api.submitScan behavior.
  • jpdevhub/FreshScanAi#106: Both modify AnalysisDashboard.tsx's scan loading logic, with overlapping offline fallback code paths.
  • jpdevhub/FreshScanAi#107: Overlaps with AnalysisDashboard.tsx and ScannerPage.tsx offline edge-scan handling and uncertainty UI.

Suggested labels: frontend, Hard

Suggested reviewers: jpdevhub

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: an offline scan queue with background sync support.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/ScannerPage.tsx (1)

371-380: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Await the capture/persist work before navigating

canvas.toBlob() is fire-and-forget here, but navigate("/analysis") is scheduled outside the callback. If the callback finishes late, AnalysisDashboard can mount without lastScanId and fall back to getLatestScan(), skipping the scan that was just saved. Move navigation behind the encode/persist path so the id is written before redirecting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ScannerPage.tsx` around lines 371 - 380, The navigation in
ScannerPage is happening before the capture/persist flow is guaranteed to
finish, so AnalysisDashboard can load before sessionStorage gets lastScanId.
Update the scan save flow around the toBlob callback so that the persist path in
the ScannerPage handler completes first, then call navigate("/analysis") only
after offlineDb.addScan, sessionStorage.setItem, and any related state updates
have finished.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/main.py`:
- Around line 543-550: The edge ONNX fusion path in main.py is treating client
values too loosely: `confidence_score` should use an explicit `None` check so a
valid `0.0` is preserved instead of falling back to `0.85`, and `fused_score`
should be validated/clamped before converting to `final_score_percent` and
storing related fields. Update the `source == "edge_onnx"` block to sanitize
`confidence_score`, `fused_score`, and any `regional_breakdown`-derived values
before building `edge_fusion`, keeping persisted scores within the expected `[0,
100]` range.
- Around line 562-582: The scan insert path in the `edge_onnx` handler is
swallowing database errors and still returning success, which causes the offline
sync flow to delete the queued scan. Update the `try/except` around
`_db().table("scans").insert(...).execute()` so failures are propagated back to
the client as a non-success response from this handler, and only return the
success payload when the insert actually completes. Make sure the response
contract used by `checkAndSyncScans` in `ScannerPage.tsx` can distinguish
failure so `offlineDb.deleteScan` is not called on a failed write.

In `@src/i18n/locales/en.json`:
- Around line 112-113: The localized sync success message is currently fixed
text and drops the synced count shown by ScannerPage.tsx. Update the syncSuccess
string in the locale JSON to use a count placeholder, then make ScannerPage.tsx
pass its existing successCount into that localized message and mirror the same
placeholder shape in the other locale files.

In `@src/lib/offlineDb.ts`:
- Around line 23-42: The openDB() helper in offlineDb opens a new IndexedDB
connection on every call and never closes it, so update the offlineDb access
pattern to reuse a cached IDBDatabase or explicitly close each connection after
the transaction finishes. Make the change in openDB() and the offlineDb methods
that call it (such as getPendingScans, submitScan, and deleteScan) so
connections are not left open across the sync loop. Also handle the IndexedDB
blocked/versionchange path by wiring the blocked event and closing stale
connections when a newer DB_VERSION is requested.

In `@src/pages/AnalysisDashboard.tsx`:
- Line 74: The AnalysisDashboard image preview is creating a blob URL that is
never cleaned up, causing a memory leak. Update the AnalysisDashboard component
to track the URL created in the code path that uses
URL.createObjectURL(found.image), then revoke it in a cleanup path such as a
useEffect return when the component unmounts or when the image changes. Keep the
fix localized around the photo_url assignment and the component’s state/effect
handling so each generated object URL is released.

In `@src/pages/ScannerPage.tsx`:
- Around line 186-193: The sync effect in ScannerPage is re-running because
checkAndSyncScans is recreated whenever syncingScans changes, which causes the
useEffect that registers the online listener to tear down and re-add on every
sync toggle. Update checkAndSyncScans and the surrounding useEffect so the
in-flight flag is stored in a useRef instead of state-derived deps, remove
syncingScans from the callback dependencies, and keep the effect stable so it
runs once on mount while still guarding against concurrent syncs.
- Around line 491-498: The SYNC NOW button in ScannerPage is gated by
navigator.onLine in render, which is not reactive to connectivity changes, and
the button is missing an explicit type. Update the ScannerPage component to
track online/offline status with state in the render path that controls
checkAndSyncScans, and add type="button" to the button element so it never
defaults to submit behavior.
- Line 149: The ScannerPage sync toast declaration creates an unused variable
because toast.loading already gets a fixed id, so remove the syncToastId binding
and keep the toast.loading call in place. Update the ScannerPage logic around
the offline sync toast so it no longer assigns the return value while preserving
the existing 'offline-sync' toast id and behavior.
- Around line 477-501: The pending offline scans banner in ScannerPage
references GlassCard without importing it, so rendering this branch can throw
when pendingCount is greater than zero. Add the missing GlassCard import
alongside the other ScannerPage imports, and verify the JSX in the pendingCount
block still matches the component usage seen in AnalysisDashboard or other
existing screens.

---

Outside diff comments:
In `@src/pages/ScannerPage.tsx`:
- Around line 371-380: The navigation in ScannerPage is happening before the
capture/persist flow is guaranteed to finish, so AnalysisDashboard can load
before sessionStorage gets lastScanId. Update the scan save flow around the
toBlob callback so that the persist path in the ScannerPage handler completes
first, then call navigate("/analysis") only after offlineDb.addScan,
sessionStorage.setItem, and any related state updates have finished.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 21924fed-6c1a-4943-aefa-fc82db6e5a87

📥 Commits

Reviewing files that changed from the base of the PR and between 97c6f33 and aa7505f.

📒 Files selected for processing (9)
  • backend/main.py
  • src/fusionInference.js
  • src/i18n/locales/bn.json
  • src/i18n/locales/en.json
  • src/i18n/locales/hi.json
  • src/lib/api.ts
  • src/lib/offlineDb.ts
  • src/pages/AnalysisDashboard.tsx
  • src/pages/ScannerPage.tsx

Comment thread backend/main.py
Comment on lines +543 to +550
if source == "edge_onnx" and fused_score is not None:
freshness = int(fused_score * 100)
conf = confidence_score or 0.85
edge_fusion = {
"final_score_percent": freshness,
"final_grade": _to_db_grade(freshness_label or "C"),
"confidence_score": conf,
"uncertain_prediction_flag": conf < 0.70,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate client-supplied edge fields; falsy confidence_score collapses to the default.

conf = confidence_score or 0.85 treats a legitimate 0.0 (falsy) as 0.85, masking a genuinely low-confidence scan. Also, fused_score is trusted unbounded: int(fused_score * 100) and regional_breakdown feed straight into the DB, so a malformed/malicious client could persist a freshness_index outside [0, 100]. Prefer explicit None checks and clamp inputs.

🛡️ Proposed fix
-        freshness = int(fused_score * 100)
-        conf = confidence_score or 0.85
+        fused_score = min(max(fused_score, 0.0), 1.0)
+        freshness = int(fused_score * 100)
+        conf = 0.85 if confidence_score is None else min(max(confidence_score, 0.0), 1.0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if source == "edge_onnx" and fused_score is not None:
freshness = int(fused_score * 100)
conf = confidence_score or 0.85
edge_fusion = {
"final_score_percent": freshness,
"final_grade": _to_db_grade(freshness_label or "C"),
"confidence_score": conf,
"uncertain_prediction_flag": conf < 0.70,
if source == "edge_onnx" and fused_score is not None:
fused_score = min(max(fused_score, 0.0), 1.0)
freshness = int(fused_score * 100)
conf = 0.85 if confidence_score is None else min(max(confidence_score, 0.0), 1.0)
edge_fusion = {
"final_score_percent": freshness,
"final_grade": _to_db_grade(freshness_label or "C"),
"confidence_score": conf,
"uncertain_prediction_flag": conf < 0.70,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 543 - 550, The edge ONNX fusion path in main.py
is treating client values too loosely: `confidence_score` should use an explicit
`None` check so a valid `0.0` is preserved instead of falling back to `0.85`,
and `fused_score` should be validated/clamped before converting to
`final_score_percent` and storing related fields. Update the `source ==
"edge_onnx"` block to sanitize `confidence_score`, `fused_score`, and any
`regional_breakdown`-derived values before building `edge_fusion`, keeping
persisted scores within the expected `[0, 100]` range.

Comment thread backend/main.py
Comment on lines +562 to +582
try:
_db().table("scans").insert(
{
"id": scan_id,
"user_id": str(current_user.id),
"final_grade": _to_db_grade(payload["grade"]),
"confidence_score": conf,
"image_type": "BODY",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": species_detected or "Rohu Carp",
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
"photo_urls": [photo_url] if photo_url else [],
}
).execute()
except Exception as exc:
print(f"DB write failed (edge_onnx): {exc}")

return {"success": True, "scan": payload}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Silent DB-write failure returns success: True, causing offline data loss.

The insert(...) is wrapped in a try/except that merely prints and falls through to return {"success": True, "scan": payload}. The offline sync loop in src/pages/ScannerPage.tsx (checkAndSyncScans) deletes the locally queued scan (offlineDb.deleteScan) on any successful response. So if the Supabase insert throws, the client discards the only copy of the scan → permanent loss. The edge_onnx path must propagate the failure (e.g., non-2xx / success: False) so the queued record is retained for retry.

🛡️ Proposed fix
         try:
             _db().table("scans").insert(
                 {
                     ...
                 }
             ).execute()
         except Exception as exc:
             print(f"DB write failed (edge_onnx): {exc}")
+            raise HTTPException(status_code=502, detail="Failed to persist edge scan; retry later.")

         return {"success": True, "scan": payload}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
_db().table("scans").insert(
{
"id": scan_id,
"user_id": str(current_user.id),
"final_grade": _to_db_grade(payload["grade"]),
"confidence_score": conf,
"image_type": "BODY",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": species_detected or "Rohu Carp",
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
"photo_urls": [photo_url] if photo_url else [],
}
).execute()
except Exception as exc:
print(f"DB write failed (edge_onnx): {exc}")
return {"success": True, "scan": payload}
try:
_db().table("scans").insert(
{
"id": scan_id,
"user_id": str(current_user.id),
"final_grade": _to_db_grade(payload["grade"]),
"confidence_score": conf,
"image_type": "BODY",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": species_detected or "Rohu Carp",
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
"photo_urls": [photo_url] if photo_url else [],
}
).execute()
except Exception as exc:
print(f"DB write failed (edge_onnx): {exc}")
raise HTTPException(status_code=502, detail="Failed to persist edge scan; retry later.")
return {"success": True, "scan": payload}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 562 - 582, The scan insert path in the
`edge_onnx` handler is swallowing database errors and still returning success,
which causes the offline sync flow to delete the queued scan. Update the
`try/except` around `_db().table("scans").insert(...).execute()` so failures are
propagated back to the client as a non-success response from this handler, and
only return the success payload when the insert actually completes. Make sure
the response contract used by `checkAndSyncScans` in `ScannerPage.tsx` can
distinguish failure so `offlineDb.deleteScan` is not called on a failed write.

Comment thread src/i18n/locales/en.json
Comment on lines +112 to +113
"syncingScans": "Syncing offline scans in background...",
"syncSuccess": "Successfully synchronized offline scans!",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the sync count in the localized success message.

ScannerPage.tsx already formats this toast with successCount, but the new locale string is fixed text. That drops the number of synced scans for localized users and weakens the progress signal. Please add a count placeholder here and mirror it in the other locale files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/locales/en.json` around lines 112 - 113, The localized sync success
message is currently fixed text and drops the synced count shown by
ScannerPage.tsx. Update the syncSuccess string in the locale JSON to use a count
placeholder, then make ScannerPage.tsx pass its existing successCount into that
localized message and mirror the same placeholder shape in the other locale
files.

Comment thread src/lib/offlineDb.ts
Comment on lines +23 to +42
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
};

request.onsuccess = (event) => {
resolve((event.target as IDBOpenDBRequest).result);
};

request.onerror = (event) => {
reject((event.target as IDBOpenDBRequest).error);
};
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

IndexedDB connections are opened per call and never closed.

Every offlineDb.* method calls openDB(), which creates a fresh IDBDatabase connection that is never close()d. Under the sync loop (one open per scan in getPendingScans/submitScan/deleteScan) these accumulate, and any future DB_VERSION bump will be blocked by the still-open connections (onupgradeneeded/versionchange never fires). Consider caching a single connection or closing it after each transaction completes, and handling the blocked event.

♻️ One option: close on transaction completion
   async addScan(scan: OfflineScan): Promise<void> {
     const db = await openDB();
     return new Promise((resolve, reject) => {
       const transaction = db.transaction(STORE_NAME, 'readwrite');
       const store = transaction.objectStore(STORE_NAME);
       const request = store.put(scan);
 
-      request.onsuccess = () => resolve();
-      request.onerror = () => reject(request.error);
+      request.onsuccess = () => resolve();
+      request.onerror = () => reject(request.error);
+      transaction.oncomplete = () => db.close();
+      transaction.onabort = () => { db.close(); reject(transaction.error); };
     });
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/offlineDb.ts` around lines 23 - 42, The openDB() helper in offlineDb
opens a new IndexedDB connection on every call and never closes it, so update
the offlineDb access pattern to reuse a cached IDBDatabase or explicitly close
each connection after the transaction finishes. Make the change in openDB() and
the offlineDb methods that call it (such as getPendingScans, submitScan, and
deleteScan) so connections are not left open across the sync loop. Also handle
the IndexedDB blocked/versionchange path by wiring the blocked event and closing
stale connections when a newer DB_VERSION is requested.

storage_temp: "0-4 C",
alert_flags: []
},
photo_url: URL.createObjectURL(found.image),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Object URL from the stored blob is never revoked (memory leak).

URL.createObjectURL(found.image) creates a blob URL that persists for the document lifetime. Each offline-scan view leaks another URL. Revoke it on unmount (e.g., track the created URL in state/ref and call URL.revokeObjectURL in an effect cleanup).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/AnalysisDashboard.tsx` at line 74, The AnalysisDashboard image
preview is creating a blob URL that is never cleaned up, causing a memory leak.
Update the AnalysisDashboard component to track the URL created in the code path
that uses URL.createObjectURL(found.image), then revoke it in a cleanup path
such as a useEffect return when the component unmounts or when the image
changes. Keep the fix localized around the photo_url assignment and the
component’s state/effect handling so each generated object URL is released.

Comment thread src/pages/ScannerPage.tsx
if (pending.length === 0) return;

setSyncingScans(true);
const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove unused syncToastId (lint error blocks CI).

toast.loading(...) already uses the fixed { id: 'offline-sync' }, so the returned id is never used. ESLint flags @typescript-eslint/no-unused-vars.

🧹 Proposed fix
-      const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
+      toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
🧰 Tools
🪛 ESLint

[error] 149-149: 'syncToastId' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ScannerPage.tsx` at line 149, The ScannerPage sync toast
declaration creates an unused variable because toast.loading already gets a
fixed id, so remove the syncToastId binding and keep the toast.loading call in
place. Update the ScannerPage logic around the offline sync toast so it no
longer assigns the return value while preserving the existing 'offline-sync'
toast id and behavior.

Source: Linters/SAST tools

Comment thread src/pages/ScannerPage.tsx
Comment on lines +186 to +193
useEffect(() => {
checkAndSyncScans();

window.addEventListener('online', checkAndSyncScans);
return () => {
window.removeEventListener('online', checkAndSyncScans);
};
}, [checkAndSyncScans]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sync effect re-invokes on every syncingScans toggle.

checkAndSyncScans depends on syncingScans, so it is recreated whenever the flag flips; the effect depends on checkAndSyncScans and calls it in its body, so each start/finish of a sync tears down and re-adds the online listener and re-triggers checkAndSyncScans(). Prefer a useRef guard for the in-flight flag and drop syncingScans from the callback deps so the effect runs once on mount.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ScannerPage.tsx` around lines 186 - 193, The sync effect in
ScannerPage is re-running because checkAndSyncScans is recreated whenever
syncingScans changes, which causes the useEffect that registers the online
listener to tear down and re-add on every sync toggle. Update checkAndSyncScans
and the surrounding useEffect so the in-flight flag is stored in a useRef
instead of state-derived deps, remove syncingScans from the callback
dependencies, and keep the effect stable so it runs once on mount while still
guarding against concurrent syncs.

Comment thread src/pages/ScannerPage.tsx
Comment on lines +477 to +501
{pendingCount > 0 && (
<div className="absolute top-4 inset-x-4 z-40">
<GlassCard className="p-3 border-l-4 border-secondary! flex items-center justify-between" variant="tonal">
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} />
<span className={`relative inline-flex rounded-full h-2 w-2 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} />
</span>
<span className="font-mono text-[0.625rem] tracking-widest text-on-surface uppercase">
{syncingScans
? t('scanner.syncingScans', 'Syncing offline scans in background...')
: `${pendingCount} ${t('scanner.pendingSyncScans', 'OFFLINE SCANS PENDING SYNC')}`}
</span>
</div>
{!syncingScans && navigator.onLine && (
<button
onClick={checkAndSyncScans}
className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer"
>
{t('scanner.syncNowButton', 'SYNC NOW')}
</button>
)}
</GlassCard>
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

GlassCard is used but never imported — this crashes the render whenever pendingCount > 0.

GlassCard is referenced at Line 479 but is not among the imports (Lines 1-19). As soon as a pending offline scan exists, this branch renders and throws a ReferenceError/JSX-undefined error, taking down the ScannerPage. Add the import (as AnalysisDashboard.tsx does).

🐛 Proposed fix
 import StatusTerminal from "../components/StatusTerminal";
 import CameraOverlay from "../components/CameraOverlay";
+import GlassCard from "../components/GlassCard";
 import { api, isAuthenticated } from "../lib/api";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{pendingCount > 0 && (
<div className="absolute top-4 inset-x-4 z-40">
<GlassCard className="p-3 border-l-4 border-secondary! flex items-center justify-between" variant="tonal">
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} />
<span className={`relative inline-flex rounded-full h-2 w-2 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} />
</span>
<span className="font-mono text-[0.625rem] tracking-widest text-on-surface uppercase">
{syncingScans
? t('scanner.syncingScans', 'Syncing offline scans in background...')
: `${pendingCount} ${t('scanner.pendingSyncScans', 'OFFLINE SCANS PENDING SYNC')}`}
</span>
</div>
{!syncingScans && navigator.onLine && (
<button
onClick={checkAndSyncScans}
className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer"
>
{t('scanner.syncNowButton', 'SYNC NOW')}
</button>
)}
</GlassCard>
</div>
)}
import GlassCard from "../components/GlassCard";
🧰 Tools
🪛 React Doctor (0.5.8)

[error] 479-479: GlassCard crashes at runtime because it isn't defined here.

Import the component or fix the typo so React can resolve the JSX identifier at runtime.

(jsx-no-undef)


[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ScannerPage.tsx` around lines 477 - 501, The pending offline scans
banner in ScannerPage references GlassCard without importing it, so rendering
this branch can throw when pendingCount is greater than zero. Add the missing
GlassCard import alongside the other ScannerPage imports, and verify the JSX in
the pendingCount block still matches the component usage seen in
AnalysisDashboard or other existing screens.

Source: Linters/SAST tools

Comment thread src/pages/ScannerPage.tsx
Comment on lines +491 to +498
{!syncingScans && navigator.onLine && (
<button
onClick={checkAndSyncScans}
className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer"
>
{t('scanner.syncNowButton', 'SYNC NOW')}
</button>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

navigator.onLine in render isn't reactive; add an explicit type="button".

The navigator.onLine check at Line 491 won't re-evaluate on connectivity changes since it isn't tied to state/an event, so the "SYNC NOW" button can be stale until an unrelated re-render. Also set type="button" on the button (Line 492) to avoid default submit semantics. Wiring an online/offline state would make this banner accurate.

🧰 Tools
🪛 React Doctor (0.5.8)

[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ScannerPage.tsx` around lines 491 - 498, The SYNC NOW button in
ScannerPage is gated by navigator.onLine in render, which is not reactive to
connectivity changes, and the button is missing an explicit type. Update the
ScannerPage component to track online/offline status with state in the render
path that controls checkAndSyncScans, and add type="button" to the button
element so it never defaults to submit behavior.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature 2: Offline Scan Queue + Background Sync for PWA

1 participant